package edu.mtu.citizenscience.gpsservice;

import android.content.Context;
import android.content.Intent;
import android.location.Criteria;
import android.location.GpsSatellite;
import android.location.Location;
import android.location.LocationListener;
import android.location.LocationManager;
import android.location.GpsStatus;
import android.os.Bundle;
import android.util.Log;

/*
 * Implementation of LocationListener
 * 
 * It provides provides locations by calling
 * 
 * 		public Location getCurrentLocation()
 * 
 * returns a Location. 
 * 
 * The Location can be null if there has never been a fix or the fix has expired. 
 * The expiration time can be set by calling 
 * 
 * 		public void setExperationTime(int t)
 * 
 * where t is the time in milliseconds for a fix to expire. The default expiration time is 10 minutes. 
 * 
 * There are two modes of location updates, tight and loose. The determine maximum update frequency. 
 * They are set by calling either
 * 
 * 		public void setLooseUpdates()
 * 		public void setTightUpdates()
 * 
 * There minimum time between updates can changed by calling
 * 
 * 		public void setMinTightTime(int t)
 *		public void setMinLoseTime(int t)
 * 
 * where t is the minimum time between updates, meaning updates should come no sooner then t. 
 * The default values are 
 * 
 * 		minTightTime = 5*1000
 * 		minLoseTime = 2*60*1000
 * 
 * Note this is only a hint for the location provider. 
 * 
 * The updates can be stopped and started by calling
 * 
 * 		public void start()
 * 		public void stop()
 * 
 * Note that when this location listener is constructed it automatically starts updates in loose mode.
 * 
 * Note using this location listener manages it own location manager ask for fine accuracy positioning. 
 * 
 * For debugging this location listener monitors the satellites. 
 * This can be turned for by changing 
 * 
 * 		private final boolean DEBUG
 * 
 * to false. 
 * 
 */
public class CSLocationListener implements LocationListener, GpsStatus.Listener {
	private final String CLASS_NAME = "CSLocationListener";
	private final boolean DEBUG = true;
	
	// Settings and fault values
	private int expirationTime = 10*60*1000; // Tolerance for location being current, if 0 ignore expiration
//	private int expirationTime = 1*60*1000; // for testing
	private int significantDeltaTime = 2*60*1000; // Tolerance for determining that the time difference significant
	private int minDistance = 0; // Minimal distance for location updates, 0 means ignore.
	private int minTightTime = 5*1000; // Minimum time between tight location updates 
	private int minLoseTime = 2*60*1000; // Minimum time between lose location updates
	private int significantDeltaAccuracy = 10; // Desired accuracy 
	
	private Context context = null;
	
	private boolean isBroadcasting = true;

	private LocationManager locationManager;
	private String provider; // location provider name
	
	private Location currentLocation = null; // current best location, except before first fix is never null
	private long currentLocationTime = 0;  // local system time for the current location
	// Using local system because I could not figure out how to get UTC time.


	public CSLocationListener(Context context)
	{
		if (DEBUG) Log.d(this.CLASS_NAME,"constructor");
		this.context = context;

		init(); // make location manager and provider
		start(); // start listening 
	}
	
	private void init()
	{
		if(this.DEBUG)Log.d(this.CLASS_NAME,"init");
		// set up location provider
		this.locationManager = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE);
		Criteria criteria = new Criteria();
		criteria.setAccuracy(Criteria.ACCURACY_FINE);
		this.provider = locationManager.getBestProvider(criteria, true);
		if (this.DEBUG && this.provider == null) Log.d("BestProvider", "null"); // If null check uses permissions in the manifest
	}
	
	public void start()
	{
		// RLP: I'm not sure we need this
		if (this.DEBUG) Log.d(this.CLASS_NAME,"start");

		
		this.setLooseUpdates();
		locationManager.addGpsStatusListener(this);
	}
		
	public void stop()
	{
		if (this.DEBUG) Log.d(this.CLASS_NAME,"stop");
		this.locationManager.removeUpdates(this);
		this.locationManager.removeGpsStatusListener(this);
	}
	
	public void setLooseUpdates()
	{
		if (this.DEBUG) Log.d(this.CLASS_NAME, "looseUpdates");
		if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
			locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minLoseTime, minDistance, this);
		}
		if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
			locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, minLoseTime, minDistance, this);
		}
	}
	
	public void setTightUpdates()
	{
		if (this.DEBUG) Log.d(this.CLASS_NAME, "tightUpdates");
		if (locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
			locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, minTightTime, minDistance, this);
		}
		if (locationManager.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) {
			locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, minTightTime, minDistance, this);
		}
	}
	

	@Override
	public void onLocationChanged(Location location) 
	{
		if (this.DEBUG) Log.d(this.CLASS_NAME,"onLocationChanged");
		if ( this.isBetterLocation(location, this.currentLocation) ) 
		{
			this.currentLocation = location;
			this.currentLocationTime = System.currentTimeMillis();
		}
		if(isBroadcasting)
		{
			Intent intent = new Intent();
			if(location != null /* && isCurrent() broadcasting service cannot really monitor currency */){
				if(this.DEBUG)Log.d(this.CLASS_NAME,"Broadcasting known");
				intent.putExtra(CSGpsService.VALID_LOCATION, true);
				intent.putExtra("longitude",this.currentLocation.getLongitude());
				intent.putExtra("latitude", this.currentLocation.getLatitude());
				intent.putExtra("time", this.currentLocationTime);  // Note this system time, maybe should be UTC time 
				intent.putExtra("accuracy", this.currentLocation.getAccuracy());
			}
			else 
			{
				if(this.DEBUG)Log.d(this.CLASS_NAME,"Broadcasting not known");
				intent.putExtra(CSGpsService.VALID_LOCATION, false);
			}
			intent.setAction(CSGpsService.LOCATION_SERVICE);
			this.context.sendBroadcast(intent);
		}
	}
	
	/*
	 * call back method, check currency 
	 * 
	 * returns currentLocation
	 * returns null if not current
	 */
	public Location getCurrentLocation()
	{
		if (this.DEBUG) Log.d(this.CLASS_NAME, "getCurrentLocation");
		if(isCurrent())return this.currentLocation;
		return null;
	}

	private boolean isCurrent() {
		if (this.expirationTime == 0) return true;
		return (System.currentTimeMillis()-this.currentLocationTime < this.expirationTime);
	}


	/** Determines whether one Location reading is better than the current Location fix
	  * @param location  The new Location that you want to evaluate
	  * @param currentBestLocation  The current Location fix, to which you want to compare the new one
	  */
	protected boolean isBetterLocation(Location location, Location currentBestLocation) {
	    if (currentBestLocation == null) {
	        // A new location is always better than no location
	        return true;
	    }
	    
	    // Check whether the new location fix is newer or older
	    long timeDelta = location.getTime() - currentBestLocation.getTime();
	    boolean isSignificantlyNewer = timeDelta > this.significantDeltaTime;
	    boolean isSignificantlyOlder = timeDelta < -this.significantDeltaTime;
	    boolean isNewer = timeDelta > 0;

	    // If it's been more than time tolerance since the current location, use the new location
	    // because the user has likely moved
	    if (isSignificantlyNewer) {
	        return true;
	    // If the new location is more than two minutes older, it must be worse
	    } else if (isSignificantlyOlder) {
	        return false;
	    }
	    
	    // Check whether the new location fix is more or less accurate
	    int accuracyDelta = (int) (location.getAccuracy() - currentBestLocation.getAccuracy());
	    boolean isLessAccurate = accuracyDelta > 0;
	    boolean isMoreAccurate = accuracyDelta < 0;
	    boolean isSignificantlyLessAccurate = accuracyDelta > this.significantDeltaAccuracy;

	    // Check if the old and new location are from the same provider
	    boolean isFromSameProvider = isSameProvider(location.getProvider(), currentBestLocation.getProvider());

	    // Determine location quality using a combination of timeliness and accuracy
	    if (isMoreAccurate) {
	        return true;
	    } else if (isNewer && !isLessAccurate) {
	        return true;
	    } 
	    else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
	        return true;
	    }
	    return false;
	}
	
	/** Checks whether two providers are the same */
	private boolean isSameProvider(String provider1, String provider2) {
	    if (provider1 == null) {
	      return provider2 == null;
	    }
	    return provider1.equals(provider2);
	}
	
	@Override
	public void onGpsStatusChanged(int event) 
	{
		if(this.DEBUG) Log.d(this.CLASS_NAME, "onGpsStatusChanged");	
		if(this.DEBUG)
		{
			GpsStatus status = locationManager.getGpsStatus(null);
			int fixTime = status.getTimeToFirstFix();
			
			Iterable<GpsSatellite> sats = status.getSatellites();
			int nFixed = 0;  int nSats = 0;
			for (GpsSatellite sat : sats)
			{
				nSats++;
				if(sat.usedInFix()) nFixed++;
			}
			Log.d("onGpsStatusChanged", "Fixed="+ nFixed +"  numSats="+ nSats+ "   fixTime="+fixTime);	
		}
	}
	
	/*
	 * Need set method for all the setting parameters
	 */
	public void setExperationTime(int t){this.expirationTime = t;}
	public void setSignificantDeltaTime(int t){this.significantDeltaTime = t;}
	public void setSignificantAccuracy(int a){this.significantDeltaAccuracy = a;}
	public void setMinDistance(int d){this.minDistance = d;}
	public void setMinTightTime(int t){this.minTightTime = t;}
	public void setMinLoseTime(int t){this.minLoseTime = t;}
	
	@Override
	public void onProviderDisabled(String provider) {}
	@Override
	public void onProviderEnabled(String provider) {}
	@Override
	public void onStatusChanged(String provider, int status, Bundle extras) {}

}
